[S05E07] CaseStudy: RxJS真實案例展示
https://www.youtube.com/watch?v=zRFb41TfO5Y&list=PL9LUW6O9WZqgUMHwDsKQf3prtqVvjGZ6S&index=16
庫存文章逼得很緊(2篇),努力看影片做筆記
不知道對初學者有沒有一點幫助,不過我個人真的學超多的
(等第30天再來混一篇心得)
今天由jiaming大大分享自身經驗,
非常的珍貴,建議「影片」配合「jiaming大大的文章」一起看
因為影片中有蠻多的討論跟講解
jiaming大大的文章不只是講結果,還有
1.目標(題目)
2.預期結果
3.拆解問題
4.思維
jiaming大大的文章
https://jiaming0708.github.io/2018/08/20/rx-sample/
AutoComplete完整範例
https://github.com/jiaming0708/RxSample
進階範例
https://github.com/jiaming0708/draw-demo
首先clone「AutoComplete完整範例」來玩
用RxJS實現WebAPI傳回來的資料重組,重組成我們要的型別
前端用input讓使用者輸入關鍵字
再打用Web API要資料「行政院環境保護署。環境資源資料開放平臺」
真的很感謝環保署
對資料科學的貢獻
練習用Web API
公民營廢棄物清除機構資料
https://opendata.epa.gov.tw/Data/Contents/WROrg
[
{
"County":"宜蘭縣",
"OrgType":"清除",
"Grade":"甲",
"OrgName":"境庭有限公司",
"RegistrationNo":"G3004187",
"OrgAddress":"宜蘭縣宜蘭市文昌路一九八之六號一樓",
"TreatMethod":"",
"GrantDeadline":"2022/7/18 上午 12:00:00",
"OrgTel":"03-9356440",
"OperatingAddress":"宜蘭縣宜蘭市文昌路一九八之六號一樓"
},...
]
相對應的data model
export interface Waste {
County: string;
OrgType: string;
Grade: string;
OrgName: string;
RegistrationNo: string;
OrgAddress: string;
TreatMethod: string;
GrantDeadline: string;
OrgTel: string;
OperatingAddress: string;
}
app.component.html
<form [formGroup]="form">
<input formControlName="keyword">
^^^^^ 輸入keywork
<ul> VVVVV 就不用做unsubscribe
<li *ngFor="let item of wastes$ | async as wastes">
{{item.OrgName}} 到環保署的WebAPI要資料,做前端filter,show出來
</li>
</ul>
Kevin建議寫法,避免沒值的情況
<ul *ngIf="wastes$ | async as wastes">
<li *ngFor="let item of wastes">
{{item.OrgName}}
</li>
</ul>
</form>
app.component.ts
export class AppComponent implements OnInit {
keyword$: Observable<string>;
form: FormGroup;
wastes$: Observable<Waste[]>;
// "OrgName":"境庭有限公司" 找OrgName欄位有沒有「keyword」
// index = -1就是沒有
hasKeyword = (keyword: string) => {
return (waste: Waste) => waste.OrgName.indexOf(keyword) > -1;
^^^^^^^
}
ngOnInit() {
this.form = this.fb.group({
keyword: ['']
});
this.form.get('keyword').valueChanges
^^^^^^^^^^^
reactive form 本身 value changes的監聽
.subscribe(p=>console.log(p));
^ 當keyword改變時,就抓的到
// 這裡的2個observable分別是this.keyword$跟this.envAPI.wasteAPI$
// switchMap就是:
// 每個this.keyword$資料流,都會丟到this.envAPI.wasteAPI$做運算
// mergeMap 我猜是把this.envAPI.wasteAPI$的結果併起來
// way1: `mergeMap`
this.wastes$ = this.keyword$.pipe(
debounceTime(200), // 每個字等0.2秒,減少送http request的次數
switchMap(keyword => this.envAPI.wasteAPI$.pipe(
^^^^^^^^ 等於 map + switch
mergeMap(p => from(p)), // Observable<Waste>
^^^^^^^^^ api吐回來的資料流合併,不管順序
filter(this.hasKeyword(keyword)),
toArray() // 把filter完的結果,用toArray()轉成陣列
))
);
// switchMap 會在下一個 observable 被送出後直接退訂前一個未處理完的 observable
// 如果新資料已回來,會取消較舊的keyword(還沒回來的),只保留最後1次
// 適合用在發送 HTTP request
// https://rxjs-dev.firebaseapp.com/api/operators/switchMap
// 看圖比較好懂
// const switched = of(1, 2, 3).pipe(switchMap((x: number) => of(x, x ** 2, x ** 3)));
^ ^ ^^^^^^ ^^^^^^
// switched.subscribe(x => console.log(x));
// 輸出: 1 1 1、2 4 8、3 9 27
// 沒有用RxJS的寫法。若用RxJS改寫,可以把每個步驟拆開來看
var result = []; // 建一個新陣列
array.forEach(p => {
^^^^^^^ 對應上面的from(p)
if(p.OrgName.indexOf(keyword) > -1){
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ filter(this.hasKeyword(keyword))
result.push(p); // 把符合條件的資料塞到陣列中
^^^^^^^^^^^^^^ toArray()
}
});
優化
// way2: combineLatest
// 2個observable:this.keyword$、this.envAPI.wasteAPI$
this.wastes$ = combineLatest(
this.keyword$.pipe(
debounceTime(200)
), this.envAPI.wasteAPI$
).pipe( // 拿最後1次的值輸出
map(([keyword, wastes]) => wastes.filter(this.hasKeyword(keyword)))
);
練習資料長這樣:
[
{
modifyTime: '2018/04/27',
criticalLevel: 3, 不一定有
oddsLevel: 3 不一定有
},
{
modifyTime: '2018/03/22', 把不完整的資料乎略
}
]
範例一:
[
{
id: `${oddsLevel}-${criticalLevel}`,
date: '', modifyTime欄位改名
order: 0 多加一個欄位
}
]
getPointList() {
return obs => obs.pipe(
// 步驟1
switchMap((p: HistoryRecord[]) => p),
^^^^^^^^^ 指定型別,接傳回來的資料,也可以用mergeMap(p => from(p))
// 步驟2
filter((p: HistoryRecord) => !!p.criticalLevel && !!p.oddsLevel),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
callback function,回傳false就不要
// 步驟3 用map()組合成想要的東西
map((p: HistoryRecord) => ({
id: `${p.oddsLevel}-${p.criticalLevel}`,
date: p.modifyTime
} as PointData)),
^^^^^^^^^ Object的型別
toArray(), // 轉陣列
map((p: PointData[]) => p.sort((a, b) => Date.parse(b.date) - Date.parse(a.date))),
^^^^ ^^^^ compareFunction(a,b)
// javascript 的 Array.prototype.sort()
// compareFunction(a, b) 若<0,a排在b前面;若=0不變;若>0,b排在a前面
// https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
map((p: PointData[]) => p.map((data, idx) => ({ ...data, order: idx })))
原陣列 ^^^^ 加欄位
);
}
範例二:
[
{
id: `${oddsLevel}-${criticalLevel}`,
total: 0, 多加一個欄位,array資料筆數
current: 0 多加一個欄位
}
]
// 常用情境:
// 如果有2個api,可以用mergeMap、reduce處理、map重新組合
getPointCount() {
vvv obs是observable,可以想像成被訂閱時,外面的資料流進來,再pipe處理
return obs => obs.pipe(
^^^ higher-order function(高階)寫法,obs是一個function
// https://jcouyang.gitbooks.io/functional-javascript/content/zh/!higher-order-function.html
vvvv 轉Observable
mergeMap((pointList: PointData[]) => from(pointList)
^^^^^^^^^^^^^^^^^^^^^^要保留在後面用,才需要用mergeMap
.pipe( // 用reduce整理資料
reduce((acc, value: PointData) => {
^^^^^^ RxJS跟javascript都有reduce
const id = value.id;
if (!acc[id]) {
acc[id] = { total: 0, current: 0 } as PointCount;
}
acc[id].total++;
return acc;
}, [] as PointCount[]),
// 把2個陣列,組成Object
map(counts => ({ pointList: pointList, pointCount: counts }))
^^^^^^^^^^^^^^^^^^^^
))
);
}
// return obs => obs.pipe(
// switchMap((p: HistoryRecord[]) => p),
// 可以寫成
// return (obs: Observable<HistoryRecord[]>) => obs.pipe(
// ^^^^^^^^^^^^^^^資料流的型別
// switchMap(p=>p) 後面的p就知道型別
// 也可以寫成
// return (obs: Observable<HistoryRecord[]>) => obs.pipe<PointData[]>(
^^^^^^^^^^^^^
// pipe支援泛型,給的型別其實就是pipe最後出來的資料長什麼樣子
// 好處是靜態分析會多個檢查
this.api.dataApi$.pipe(
filter(p => !!p && p.length > 0),
this.getPointList(), // 回傳型別 :(obs:any)=>any
this.getPointCount()
).subscribe( (p:{pointList, pointCount}) => {
^^^^^^^^^^ ^^^^^^^^^^^
// 這個邊才告知getPointList()跟getPointCount()回傳的型別(資料長什麼樣子)
// 如果沒正常回傳會報錯嗎? 是否要串catchError()?
this.api.pointList$.next(p.pointList);
});
範例二預期輸出:
輸出一個Object,這個Object包含2個Array(練習一、練習二)
資料流是從API下來,假設經過component處理後輸出
我們要監聽這些資料流所經過component的資料(經過幾個component就會有幾筆資料)
理後的資料
範例三
// 1
{
top: 10, // 經過的component的top
left: 10, // component的left
order: 0
}
// 2
{
top: 20,
left: 20,
order: 1
}…
最後期望的資料
第一組 第二組 第三組
[[1, 2], [2, 3], [3, 4]]
^ ^
(最後期望的資料)
吐出來怎麼知道WebAPI的資料已取完?
this.api.pointList$
.pipe(
mergeMap(pointList =>
this.api.elementPoint$
.pipe(
bufferCount(pointList.length),
tap(p=>console.log(p)), // bufferCount吐出來的是array
switchMap(p => p.sort((a, b) => a.order - b.order)),
pairwise(),
bufferCount(pointList.length - 1, 1), // 這行看不懂
起始位置 取幾筆
// 如果沒有bufferCount,會全取所有資料
)))
.subscribe(p => console.log(p));